package com.gitee.fastmybatis.core.support.plugin;


import com.gitee.fastmybatis.core.FastmybatisConfig;
import com.gitee.fastmybatis.core.util.ClassUtil;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.sql.PreparedStatement;
import java.util.Objects;
import java.util.Properties;
import java.util.function.Supplier;

/**
 * SQL美化插件
 */
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class})
})
public class SqlFormatterPlugin implements Interceptor {

    private static final Log LOG = LogFactory.getLog(SqlFormatterPlugin.class);

    private static Supplier<SqlFormatterPlugin> builder = SqlFormatterPlugin::new;

    private final SqlFormatter sqlFormatter = new SqlFormatter();

    private SqlFormatterHandler sqlFormatterHandler;

    private Properties properties;

    /**
     * 打印SQL末尾是否自动添加结束符（;）
     */
    private boolean appendDelimiter = true;

    /**
     * 结束符
     */
    private String delimiter = ";";

    /**
     * 是否使用Druid
     */
    private boolean useDruid = false;

    private String format = "\n====== SQL ======\n%s";

    private boolean enable = false;

    public static Interceptor create() {
        SqlFormatterPlugin sqlFormatterPlugin = builder.get();
        Properties properties = new Properties();
        sqlFormatterPlugin.setProperties(properties);
        return sqlFormatterPlugin;
    }

    public static void setBuilder(Supplier<SqlFormatterPlugin> builder) {
        SqlFormatterPlugin.builder = builder;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            return invocation.proceed();
        } finally {
            printSql(invocation);
        }
    }

    protected void printSql(Invocation invocation) throws Exception {
        if (this.sqlFormatterHandler != null) {
            sqlFormatterHandler.doPrintSql(invocation);
        } else {
            if (canPrint(invocation)) {
                String sql = getSql(invocation);
                sql = formatSql(sql);
                printSql(sql);
            }
        }
    }


    @Override
    public void setProperties(Properties properties) {
        Objects.requireNonNull(properties, "properties can not null");
        this.properties = properties;
        this.appendDelimiter = Boolean.parseBoolean(properties.getProperty("appendDelimiter", String.valueOf(this.appendDelimiter)));
        this.enable = Boolean.parseBoolean(properties.getProperty("enable", String.valueOf(this.enable)));
        this.useDruid = Boolean.parseBoolean(properties.getProperty("useDruid", String.valueOf(this.useDruid)));
        this.delimiter = properties.getProperty("delimiter", String.valueOf(this.delimiter));
        this.format = properties.getProperty("format", String.valueOf(this.format));

        String handlerClass = properties.getProperty("handlerClass");
        if (handlerClass != null) {
            try {
                Object obj = ClassUtil.newInstance(Class.forName(handlerClass));
                if (!(obj instanceof SqlFormatterHandler)) {
                    throw new RuntimeException(handlerClass + " should implements SqlFormatterHandler");
                }
                this.sqlFormatterHandler = ((SqlFormatterHandler) obj);
            } catch (Exception e) {
                LOG.warn("Init SqlFormatterHandler error:" + e.getMessage());
                this.sqlFormatterHandler = new DefaultSqlFormatterHandler();
            }
            this.sqlFormatterHandler.setProperties(properties);
        }
    }


    public boolean canPrint(Invocation invocation) {
        return this.enable || FastmybatisConfig.PRINT_SQL;
    }

    public String getSql(Invocation invocation) throws Exception {
        PreparedStatement statement = (PreparedStatement) invocation.getArgs()[0];
        if (Proxy.isProxyClass(statement.getClass())) {
            InvocationHandler handler = Proxy.getInvocationHandler(statement);
            if (handler.getClass().getName().endsWith(".PreparedStatementLogger")) {
                Field field = handler.getClass().getDeclaredField("statement");
                field.setAccessible(true);
                statement = (PreparedStatement) field.get(handler);
                //原文参考：https://www.zhangshengrong.com/p/OgN5DgLDan/
                //以下应该是使用了 druid 原因 ，没有使用的话下面几行代码请注释掉
                if (isUseDruid()) {
                    Field stmt = statement.getClass().getDeclaredField("stmt");
                    stmt.setAccessible(true);
                    statement = (PreparedStatement) stmt.get(statement);
                    Field clientStatement = statement.getClass().getDeclaredField("statement");
                    clientStatement.setAccessible(true);
                    statement = (PreparedStatement) clientStatement.get(statement);
                }
            }
        }
        String statementSql = statement.toString();
        String sql = statementSql
                .replace("** BYTE ARRAY DATA **", "null")
                .substring(statementSql.indexOf(":") + 1);
        String endMark = getDelimiter();
        if (isAppendDelimiter() && !sql.endsWith(endMark)) {
            sql = sql + endMark;
        }
        return sql;
    }

    public String formatSql(String sql) {
        String fullSql = getSqlFormatter().formatQuietly(sql);
        // 自定义格式化
        return String.format(getFormat(), fullSql);
    }

    public void printSql(String sql) {
        LOG.warn(sql);
    }

    public void setEnable(boolean enable) {
        this.enable = enable;
    }

    public SqlFormatter getSqlFormatter() {
        return sqlFormatter;
    }

    public Properties getProperties() {
        return properties;
    }

    public boolean isAppendDelimiter() {
        return appendDelimiter;
    }

    public void setAppendDelimiter(boolean appendDelimiter) {
        this.appendDelimiter = appendDelimiter;
    }

    public String getDelimiter() {
        return delimiter;
    }

    public void setDelimiter(String delimiter) {
        this.delimiter = delimiter;
    }

    public boolean isUseDruid() {
        return useDruid;
    }

    public void setUseDruid(boolean useDruid) {
        this.useDruid = useDruid;
    }

    public String getFormat() {
        return format;
    }

    public void setFormat(String format) {
        this.format = format;
    }

}
